// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/events/android/scroller.h"

#include <cmath>

#include "base/lazy_instance.h"
#include "base/macros.h"

namespace ui {
namespace {

    // Default scroll duration from android.widget.Scroller.
    const int kDefaultDurationMs = 250;

    // Default friction constant in android.view.ViewConfiguration.
    const float kDefaultFriction = 0.015f;

    // == std::log(0.78f) / std::log(0.9f)
    const float kDecelerationRate = 2.3582018f;

    // Tension lines cross at (kInflexion, 1).
    const float kInflexion = 0.35f;

    const float kEpsilon = 1e-5f;

    // Fling scroll is stopped when the scroll position is |kThresholdForFlingEnd|
    // pixels or closer from the end.
    const float kThresholdForFlingEnd = 0.1;

    bool ApproxEquals(float a, float b)
    {
        return std::abs(a - b) < kEpsilon;
    }

    struct ViscosityConstants {
        ViscosityConstants()
            : viscous_fluid_scale_(8.f)
            , viscous_fluid_normalize_(1.f)
        {
            viscous_fluid_normalize_ = 1.0f / ApplyViscosity(1.0f);
        }

        float ApplyViscosity(float x)
        {
            x *= viscous_fluid_scale_;
            if (x < 1.0f) {
                x -= (1.0f - std::exp(-x));
            } else {
                float start = 0.36787944117f; // 1/e == exp(-1)
                x = 1.0f - std::exp(1.0f - x);
                x = start + x * (1.0f - start);
            }
            x *= viscous_fluid_normalize_;
            return x;
        }

    private:
        // This controls the intensity of the viscous fluid effect.
        float viscous_fluid_scale_;
        float viscous_fluid_normalize_;

        DISALLOW_COPY_AND_ASSIGN(ViscosityConstants);
    };

    struct SplineConstants {
        SplineConstants()
        {
            const float kStartTension = 0.5f;
            const float kEndTension = 1.0f;
            const float kP1 = kStartTension * kInflexion;
            const float kP2 = 1.0f - kEndTension * (1.0f - kInflexion);

            float x_min = 0.0f;
            float y_min = 0.0f;
            for (int i = 0; i < NUM_SAMPLES; i++) {
                const float alpha = static_cast<float>(i) / NUM_SAMPLES;

                float x_max = 1.0f;
                float x, tx, coef;
                while (true) {
                    x = x_min + (x_max - x_min) / 2.0f;
                    coef = 3.0f * x * (1.0f - x);
                    tx = coef * ((1.0f - x) * kP1 + x * kP2) + x * x * x;
                    if (ApproxEquals(tx, alpha))
                        break;
                    if (tx > alpha)
                        x_max = x;
                    else
                        x_min = x;
                }
                spline_position_[i] = coef * ((1.0f - x) * kStartTension + x) + x * x * x;

                float y_max = 1.0f;
                float y, dy;
                while (true) {
                    y = y_min + (y_max - y_min) / 2.0f;
                    coef = 3.0f * y * (1.0f - y);
                    dy = coef * ((1.0f - y) * kStartTension + y) + y * y * y;
                    if (ApproxEquals(dy, alpha))
                        break;
                    if (dy > alpha)
                        y_max = y;
                    else
                        y_min = y;
                }
                spline_time_[i] = coef * ((1.0f - y) * kP1 + y * kP2) + y * y * y;
            }
            spline_position_[NUM_SAMPLES] = spline_time_[NUM_SAMPLES] = 1.0f;
        }

        void CalculateCoefficients(float t,
            float* distance_coef,
            float* velocity_coef)
        {
            *distance_coef = 1.f;
            *velocity_coef = 0.f;
            const int index = static_cast<int>(NUM_SAMPLES * t);
            if (index < NUM_SAMPLES) {
                const float t_inf = static_cast<float>(index) / NUM_SAMPLES;
                const float t_sup = static_cast<float>(index + 1) / NUM_SAMPLES;
                const float d_inf = spline_position_[index];
                const float d_sup = spline_position_[index + 1];
                *velocity_coef = (d_sup - d_inf) / (t_sup - t_inf);
                *distance_coef = d_inf + (t - t_inf) * *velocity_coef;
            }
        }

    private:
        enum { NUM_SAMPLES = 100 };

        float spline_position_[NUM_SAMPLES + 1];
        float spline_time_[NUM_SAMPLES + 1];

        DISALLOW_COPY_AND_ASSIGN(SplineConstants);
    };

    float ComputeDeceleration(float friction)
    {
        const float kGravityEarth = 9.80665f;
        return kGravityEarth // g (m/s^2)
            * 39.37f // inch/meter
            * 160.f // pixels/inch
            * friction;
    }

    template <typename T>
    int Signum(T t)
    {
        return (T(0) < t) - (t < T(0));
    }

    template <typename T>
    T Clamped(T t, T a, T b)
    {
        return t < a ? a : (t > b ? b : t);
    }

    // Leaky to allow access from the impl thread.
    base::LazyInstance<ViscosityConstants>::Leaky g_viscosity_constants = LAZY_INSTANCE_INITIALIZER;

    base::LazyInstance<SplineConstants>::Leaky g_spline_constants = LAZY_INSTANCE_INITIALIZER;

} // namespace

Scroller::Config::Config()
    : fling_friction(kDefaultFriction)
    , flywheel_enabled(false)
{
}

Scroller::Scroller(const Config& config)
    : mode_(UNDEFINED)
    , start_x_(0)
    , start_y_(0)
    , final_x_(0)
    , final_y_(0)
    , min_x_(0)
    , max_x_(0)
    , min_y_(0)
    , max_y_(0)
    , curr_x_(0)
    , curr_y_(0)
    , duration_seconds_reciprocal_(1)
    , delta_x_(0)
    , delta_x_norm_(1)
    , delta_y_(0)
    , delta_y_norm_(1)
    , finished_(true)
    , flywheel_enabled_(config.flywheel_enabled)
    , velocity_(0)
    , curr_velocity_(0)
    , distance_(0)
    , fling_friction_(config.fling_friction)
    , deceleration_(ComputeDeceleration(fling_friction_))
    , tuning_coeff_(ComputeDeceleration(0.84f))
{
}

Scroller::~Scroller()
{
}

bool Scroller::ComputeScrollOffset(base::TimeTicks time,
    gfx::Vector2dF* offset,
    gfx::Vector2dF* velocity)
{
    DCHECK(offset);
    DCHECK(velocity);
    if (!ComputeScrollOffsetInternal(time)) {
        *offset = gfx::Vector2dF(GetFinalX(), GetFinalY());
        *velocity = gfx::Vector2dF();
        return false;
    }

    *offset = gfx::Vector2dF(GetCurrX(), GetCurrY());
    *velocity = gfx::Vector2dF(GetCurrVelocityX(), GetCurrVelocityY());
    return true;
}

void Scroller::StartScroll(float start_x,
    float start_y,
    float dx,
    float dy,
    base::TimeTicks start_time)
{
    StartScroll(start_x,
        start_y,
        dx,
        dy,
        start_time,
        base::TimeDelta::FromMilliseconds(kDefaultDurationMs));
}

void Scroller::StartScroll(float start_x,
    float start_y,
    float dx,
    float dy,
    base::TimeTicks start_time,
    base::TimeDelta duration)
{
    DCHECK_GT(duration.ToInternalValue(), 0);
    mode_ = SCROLL_MODE;
    finished_ = false;
    duration_ = duration;
    duration_seconds_reciprocal_ = 1.0 / duration_.InSecondsF();
    start_time_ = start_time;
    curr_x_ = start_x_ = start_x;
    curr_y_ = start_y_ = start_y;
    final_x_ = start_x + dx;
    final_y_ = start_y + dy;
    RecomputeDeltas();
    curr_time_ = start_time_;
}

void Scroller::Fling(float start_x,
    float start_y,
    float velocity_x,
    float velocity_y,
    float min_x,
    float max_x,
    float min_y,
    float max_y,
    base::TimeTicks start_time)
{
    DCHECK(velocity_x || velocity_y);

    // Continue a scroll or fling in progress.
    if (flywheel_enabled_ && !finished_) {
        float old_velocity_x = GetCurrVelocityX();
        float old_velocity_y = GetCurrVelocityY();
        if (Signum(velocity_x) == Signum(old_velocity_x) && Signum(velocity_y) == Signum(old_velocity_y)) {
            velocity_x += old_velocity_x;
            velocity_y += old_velocity_y;
        }
    }

    mode_ = FLING_MODE;
    finished_ = false;

    float velocity = std::sqrt(velocity_x * velocity_x + velocity_y * velocity_y);

    velocity_ = velocity;
    duration_ = GetSplineFlingDuration(velocity);
    DCHECK_GT(duration_.ToInternalValue(), 0);
    duration_seconds_reciprocal_ = 1.0 / duration_.InSecondsF();
    start_time_ = start_time;
    curr_time_ = start_time_;
    curr_x_ = start_x_ = start_x;
    curr_y_ = start_y_ = start_y;

    float coeff_x = velocity == 0 ? 1.0f : velocity_x / velocity;
    float coeff_y = velocity == 0 ? 1.0f : velocity_y / velocity;

    double total_distance = GetSplineFlingDistance(velocity);
    distance_ = total_distance * Signum(velocity);

    min_x_ = min_x;
    max_x_ = max_x;
    min_y_ = min_y;
    max_y_ = max_y;

    final_x_ = start_x + total_distance * coeff_x;
    final_x_ = Clamped(final_x_, min_x_, max_x_);

    final_y_ = start_y + total_distance * coeff_y;
    final_y_ = Clamped(final_y_, min_y_, max_y_);

    RecomputeDeltas();
}

void Scroller::ExtendDuration(base::TimeDelta extend)
{
    base::TimeDelta passed = GetTimePassed();
    duration_ = passed + extend;
    duration_seconds_reciprocal_ = 1.0 / duration_.InSecondsF();
    finished_ = false;
}

void Scroller::SetFinalX(float new_x)
{
    final_x_ = new_x;
    finished_ = false;
    RecomputeDeltas();
}

void Scroller::SetFinalY(float new_y)
{
    final_y_ = new_y;
    finished_ = false;
    RecomputeDeltas();
}

void Scroller::AbortAnimation()
{
    curr_x_ = final_x_;
    curr_y_ = final_y_;
    curr_velocity_ = 0;
    curr_time_ = start_time_ + duration_;
    finished_ = true;
}

void Scroller::ForceFinished(bool finished)
{
    finished_ = finished;
}

bool Scroller::IsFinished() const
{
    return finished_;
}

base::TimeDelta Scroller::GetTimePassed() const
{
    return curr_time_ - start_time_;
}

base::TimeDelta Scroller::GetDuration() const
{
    return duration_;
}

float Scroller::GetCurrX() const
{
    return curr_x_;
}

float Scroller::GetCurrY() const
{
    return curr_y_;
}

float Scroller::GetCurrVelocity() const
{
    if (finished_)
        return 0;
    if (mode_ == FLING_MODE)
        return curr_velocity_;
    return velocity_ - deceleration_ * GetTimePassed().InSecondsF() * 0.5f;
}

float Scroller::GetCurrVelocityX() const
{
    return delta_x_norm_ * GetCurrVelocity();
}

float Scroller::GetCurrVelocityY() const
{
    return delta_y_norm_ * GetCurrVelocity();
}

float Scroller::GetStartX() const
{
    return start_x_;
}

float Scroller::GetStartY() const
{
    return start_y_;
}

float Scroller::GetFinalX() const
{
    return final_x_;
}

float Scroller::GetFinalY() const
{
    return final_y_;
}

bool Scroller::IsScrollingInDirection(float xvel, float yvel) const
{
    return !finished_ && Signum(xvel) == Signum(delta_x_) && Signum(yvel) == Signum(delta_y_);
}

bool Scroller::ComputeScrollOffsetInternal(base::TimeTicks time)
{
    if (finished_)
        return false;

    if (time <= start_time_)
        return true;

    if (time == curr_time_)
        return true;

    base::TimeDelta time_passed = time - start_time_;
    if (time_passed >= duration_) {
        AbortAnimation();
        return false;
    }

    curr_time_ = time;

    const float u = time_passed.InSecondsF() * duration_seconds_reciprocal_;
    switch (mode_) {
    case UNDEFINED:
        NOTREACHED() << "|StartScroll()| or |Fling()| must be called prior to "
                        "scroll offset computation.";
        return false;

    case SCROLL_MODE: {
        float x = g_viscosity_constants.Get().ApplyViscosity(u);

        curr_x_ = start_x_ + x * delta_x_;
        curr_y_ = start_y_ + x * delta_y_;
    } break;

    case FLING_MODE: {
        float distance_coef = 1.f;
        float velocity_coef = 0.f;
        g_spline_constants.Get().CalculateCoefficients(
            u, &distance_coef, &velocity_coef);

        curr_velocity_ = velocity_coef * distance_ * duration_seconds_reciprocal_;

        curr_x_ = start_x_ + distance_coef * delta_x_;
        curr_x_ = Clamped(curr_x_, min_x_, max_x_);

        curr_y_ = start_y_ + distance_coef * delta_y_;
        curr_y_ = Clamped(curr_y_, min_y_, max_y_);

        float diff_x = std::abs(curr_x_ - final_x_);
        float diff_y = std::abs(curr_y_ - final_y_);
        if (diff_x < kThresholdForFlingEnd && diff_y < kThresholdForFlingEnd)
            AbortAnimation();
    } break;
    }

    return !finished_;
}

void Scroller::RecomputeDeltas()
{
    delta_x_ = final_x_ - start_x_;
    delta_y_ = final_y_ - start_y_;

    const float hyp = std::sqrt(delta_x_ * delta_x_ + delta_y_ * delta_y_);
    if (hyp > kEpsilon) {
        delta_x_norm_ = delta_x_ / hyp;
        delta_y_norm_ = delta_y_ / hyp;
    } else {
        delta_x_norm_ = delta_y_norm_ = 1;
    }
}

double Scroller::GetSplineDeceleration(float velocity) const
{
    return std::log(kInflexion * std::abs(velocity) / (fling_friction_ * tuning_coeff_));
}

base::TimeDelta Scroller::GetSplineFlingDuration(float velocity) const
{
    const double l = GetSplineDeceleration(velocity);
    const double decel_minus_one = kDecelerationRate - 1.0;
    const double time_seconds = std::exp(l / decel_minus_one);
    return base::TimeDelta::FromMicroseconds(time_seconds * base::Time::kMicrosecondsPerSecond);
}

double Scroller::GetSplineFlingDistance(float velocity) const
{
    const double l = GetSplineDeceleration(velocity);
    const double decel_minus_one = kDecelerationRate - 1.0;
    return fling_friction_ * tuning_coeff_ * std::exp(kDecelerationRate / decel_minus_one * l);
}

} // namespace ui
